
// Copyright (C) 2007 Richard Uhler
// This file is part of Esiply.
//
// Esiply is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License, Version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

// This file contains the implementation for all the String standard
// library stuff that can be implemented completely in javascript

// Esiply.set_attributes(this, "String", Esiply.DONT_ENUM);
String = Esiply.create_janus_function(

// Ecma 15.5.1.1 String([value])
// Retuns a string value (not a String object) computed by toString(value).
// If value is not suplpied, the empty string "" is returned
function(value)
{
    if (arguments.length == 0)
    {
        return "";
    }
    return Esiply.to_string(value);
},

// Ecma 15.5.2.1 new String([value])
function(value)
{
    var obj = {};

    // The [[prototype]] property is set to the string prototype
    Esiply.set_prototype(obj, String.prototype);

    // The [[class]] property is set to "String"
    Esiply.set_class(obj, "String");

    // The [[value]] property is set to toString(value), or to the empty
    // string ig value is not supplied
    var value = arguments.length == 0 ? "" : String(value);
    Esiply.set_value(obj, value);

    obj.length = Esiply.string_length(value);
    return obj;
});

// Ecma 15.5.3.1 String.prototype
// The initial value of String.prototype is the String prototype object.
// This property has the attributes {DontEnum, DontDelete, ReadOnly}
String.prototype = Esiply.string_prototype;
Esiply.set_attributes(String, "prototype",
        Esiply.DONT_ENUM | Esiply.DONT_DELETE | Esiply.READ_ONLY);

// Ecma 15.5.3.2 String.fromCharCode([char0[,char1[,...]]])
// Returns a string value containing as many characters as the
// number of arguments. Each argument specifies one character of the
// resulting string, with the first argument specifying the first character,
// and so on, from left to right. An argument is converted to a character
// by applying the operation toUint16 and regarding the resulting 16-bit 
// integer as the code point value of a character. If no arguments are 
// supplied, the result is the empty string.
Esiply.set_attributes(String, "fromCharCode", Esiply.DONT_ENUM);
String.fromCharCode = function(char0)
{
    var str = "";
    for (var i = 0; i < arguments.length; i++)
    {
        str += Esiply.charFromCode(arguments[i]);
    }
    return str;
};

// Ecma 15.5.4.1 String.prototype.constructor
// The initial value of String.prototype.constructor is the built-in String
// constructor
Esiply.set_attributes(String.prototype, "constructor", Esiply.DONT_ENUM);
String.prototype.constructor = String;

// Ecma 15.5.4.2 String.prototype.toString()
// Returns this string value. The toString function is not generic; it
// throws a TypeError exception if its this value is not
// a String object. Therefore it cannot be transferred to other kinds of
// objects for use as a method
// This is just the same as String.prototype.valueOf
Esiply.set_attributes(String.prototype, "toString", Esiply.DONT_ENUM);
String.prototype.toString = function()
{
    return this.valueOf();
};

// Ecma 15.5.4.2 String.prototype.valueOf()
// Returns this string value. The valueOf function is not generic; it
// throws a TypeError exception if its this value is not
// a String object. Therefore it cannot be transferred to other kinds of
// objects for use as a method
Esiply.set_attributes(String.prototype, "valueOf", Esiply.DONT_ENUM);
String.prototype.valueOf = function()
{
    if (Esiply.get_class(this) != "String")
    {
        throw new TypeError("String.prototype.valueOf passed non-String type");
    }
    return Esiply.get_value(this);
};

// Ecma 15.5.4.4 String.prototype.charAt(pos)
// Returns a string containing the character at position pos in the string
// resulting from converting this object to a string. If there is no
// character at that position, the result is the empty string. The result is
// a string value, not a String object
Esiply.set_attributes(String.prototype, "charAt", Esiply.DONT_ENUM);
String.prototype.charAt = function(pos)
{
    // 1. Call toString, giving it the this value as its argument
    var str = String(this);

    // 2. Call toInteger(pos)
    // TODO: toInteger!
    pos = Number(pos);

    // 3. Compute the number of characters in 1
    var len = str.length;

    // 4. If pos is less than 0 or is not less than len, return the
    // empty string.
    if (pos < 0 || pos >= len)
    {
        return "";
    }

    return String.fromCharCode(str.charCodeAt(pos));
};

// Ecma 15.5.4.5 String.prototype.charCodeAt(pos)
// Returns a number (a nonnegative integer less than 2^16) representing
// the code point value of the character at position pos in the string
// resulting from converting this object to a string. If there is no
// character at that position, the result is NaN.
Esiply.set_attributes(String.prototype, "charCodeAt", Esiply.DONT_ENUM);
String.prototype.charCodeAt = function(pos)
{
    // 1. Call toString, giving it the this value as its argument.
    var str = String(this);

    // 2. Call toInteger (pos)
    pos = Math.floor(pos);

    // 3. Compute the number of characters in str
    var len = str.length;

    // 4. If res2 is less than 0 or is not less than res3, return NaN
    if (pos < 0 || pos >= len)
    {
        return NaN;
    }

    // 5. Return a value of Number type, whose value is the code point value
    // of the character at position Result(2) in the string Result(1), where
    // the first (leftmost) character in result(1) is considered to
    // be at position 0, the next one at position 1, and so on.
    return Esiply.charCodeAt(str, pos);
};

// Ecma 15.5.4.6 String.prototype.concat([string1 [,string2 [,...]]])
// When the concat method is called with zero or more arguments string1,
// string2, etc., it returns a string consisting of the characters of
// this object (converted to a string) followed by the characters
// of each of string1, string2, etc. (where each argument is converted to a
// string). The result is a string value, not a String object.
Esiply.set_attributes(String.prototype, "concat", Esiply.DONT_ENUM);
String.prototype.concat = function(obj)
{
    var str = "" + this;
    for (var i = 0; i < arguments.length; i++)
    {
        str += "" + arguments[i];
    }
    return str;
};

// Ecma 15.5.4.7 String.prototype.indexOf(searchString, position)
// If searchString appears as a substring of the result of converting
// this object to a string , at one or more positions that are greater
// than or equal to position, then the index of the smallest such position
// is returned; otherwise, -1 is returned. If position is undefined, 0 is
// assumed, so as to search all of the string.
Esiply.set_attributes(String.prototype, "indexOf", Esiply.DONT_ENUM);
String.prototype.indexOf = function(searchString, position)
{
    // 1. call toString, giving it the this value as its argument
    var str = String(this);

    // 2. call toString(searchString)
    var search = String(searchString);

    // 3. call toInteger(position)
    var pos = Math.floor(Number(position == undefined ? 0 : position));

    // 4. Compute the number of chars in str
    var strlen = str.length;

    // 5. Compute min(max(result(3),0),resul(4))
    pos = Math.min(Math.max(pos, 0), strlen);

    // 6. compute number of chars in result(2)
    var searchlen = search.length;

    for (var i = pos; i < strlen; i++)
    {
        if (String(str.substring(i, i + searchlen)) == search)
        {
            return i;
        }
    }
    return -1;
};

// Ecma 15.5.4.8 String.prototype.lastIndexOf(searchString, position)
// If searchString appears as a substring of the result of converting this
// object to a string at one or more positions that are smaller than or 
// equal to position, then the index of the greatest such position is
// returned; otherwise, -1 is returned. If position is undefined, the
// length of the string value is assumed, so as to seach all of the string.
Esiply.set_attributes(String.prototype, "lastIndexOf", Esiply.DONT_ENUM);
String.prototype.lastIndexOf = function(search, pos)
{
    // 1. Call toString, giving it the this value as its argument
    var str = String(this);

    // 2. Call toString search
    search = String(search);

    // 3. Call toNumber(pos)
    pos = +pos;

    // 4. If result 3 is NaN, use +infinity, otherwise, call toInteger res3
    pos = isNan(pos) ? Infinity : Math.floor(res3);

    // 5. Compute the number of characters in Res(1)
    var len = str.length;

    // 6. Compute min(max(res4,0), res5)
    pos = Math.min(Math.max(pos, 0), len);

    // 7. Compute the number of characters in res2
    var searchlen = search.length;

    // 8. blah blah
    for (var i = pos-1; i >= 0; i--)
    {
        if (String(str.substring(i, i + searchlen)) == search)
        {
            return i;
        }
    }
    return -1;
};

// Ecma 15.5.4.10 String.prototype.match(regexp)
Esiply.set_attributes(String.prototype, "match", Esiply.DONT_ENUM);
String.prototype.match = function(regexp)
{
    // If regexp is not an object whose [[class]] property is "RegExp",
    // it is replaced with the result of the expression new RegExp(regexp)
    if (Esiply.get_class(regexp) != "RegExp")
    {
        regexp = new RegExp(regexp);
    }

    // Let string denote the result of converting the this value to a string.
    var string = String(this);

    // If regexp.global is false, return the result obtained by invoking
    // RegExp.prototype.exec on regexp with string as parameter
    if (!regexp.global)
    {
        return regexp.exec(string);
    }

    // If regexp.global is true, set the regexp.lastIndex property to 0 and
    // invoke RegExp.prototype.exec repeatedly until there is no match. If
    // there is a match with an empty string (in other words, if the value of
    // regexp.lastIndex is left unchanged), increment regexp.lastIndex by 1.
    // Let n be the number of matches. The value returned is an array with
    // the length property set to n and properties 0 through n-1 corresponding
    // to the first elements of the results of all matching invocations of 
    // RegExp.prototype.exec.
    var res = new Array();
    regexp.lastIndex = 0;
    for (var match = regexp.exec(string); match != null; match = regexp.exec(string))
    {
        res.push(match[0]);
        if (match[0] == "")
        {
            regexp.lastIndex++;
        }
    }
    return res;
};

// Ecma 15.5.4.11 String.prototype.replace(searchValue, replaceValue)
// TODO: implement this function
Esiply.set_attributes(String.prototype, "replace", Esiply.DONT_ENUM);
String.prototype.replace = function(searchValue, replaceValue)
{
    throw new Error("String.prototype.replace: Not Yet Implemented");
};

// Ecma 15.5.4.12 String.prototype.search(regexp)
// The value string is searched from its beginning for an occurrence of
// the regular expression pattern regexp. The result is a number indicating
// the offset within the string where the pattern matched, or -1
// if there was no match.
Esiply.set_attributes(String.prototype, "search", Esiply.DONT_ENUM);
String.prototype.search = function(regexp)
{
    // If regexp is not an object whose [[class]] property is "RegExp", it
    // is replaced with the result of the expression new RegExp(regexp)
    if (Esiply.get_class(regexp) != "RegExp")
    {
        regexp = new RegExp(regexp);
    }

    // Let str denote the result of converting the this value to a string.
    var str = String(this);

    var match = regexp.exec(str);
    return match == null ? -1 : match.index;

};


// Ecma 15.5.4.13 String.prototype.slice(start, end)
// The slice method takes two arguments, start and end, and returns a substring
// of the result of converting this object to a string, starting from 
// character position start and running to, but not including, character
// position end (or through the end of the string if end is undefined). If
// start is negative, it is treated as (sourceLength+start) where sourceLength
// is the length of the string. If end is negative, it is treated as
// (sourceLength + end) where sourceLength is the length of the string. The
// result is a string value, not a String object.
Esiply.set_attributes(String.prototype, "slice", Esiply.DONT_ENUM);
String.prototype.slice = function(start, end)
{
    // 1. Call toString, giving it the this value as its argument
    var str = String(this);

    // 2. Compute the number of characters in result(1)
    var len = str.length;

    // 3. Call toInteger(start)
    start = Math.floor(start);

    // 4. If end is undefined, use res2; else use toInteger(end)
    end = (end == undefined) ? len : Math.floor(end);

    // 5. If res3 is negative, use max(res2+res3),0); else use min(res3, res2)
    start = (start < 0) ? Math.max(len+start, 0) : Math.min(start,len);

    // 6. If res4 is negative, use max(res2+res4,0); else use min(res4,res2)
    end = (end < 0) ? Math.max(len+end, 0) : Math.min(end, len);

    // 7. Compute max(res6-res5, 0);
    var sublen = Math.max(end - start, 0); 

    // 8. Return a string containing Result(7) consecutive characters
    // from res(1) beginning with the character at position res5
    return str.substring(start, start + sublen);
};

// Ecma 15.5.4.14 String.prototype.split(separator, limit)
// Returns an Array object into which substrings of the result of 
// converting this object to a string have been stored. The substrings
// are determined by searching from left to right for occurrences
// of separator; these occurrences are not part of any substring in the
// returned array, but server to divide up the string value. The value of
// separator may  be a string of any length or it may be a RegExp object
// 
// The value of separator may be an empty string, an empty regular expression,
// or a regular expression that can match an empty string. In this case, 
// separator does not match the empty substring at the beginning or
// end of the input string, nor does it match the empty substring at the
// end of the previous separator match. Blah blah blah blah blah. Read
// the spec if you're curious about what you're missing.
Esiply.set_attributes(String.prototype, "split", Esiply.DONT_ENUM);
String.prototype.split = function(separator, limit)
{
    debug("split: separator: " + separator + "\n");
    debug("split: limit: " + limit + "\n");

    // The internal helper function SplitMatch takes three parameters, a
    // string S, an interger q, and a string or RegExp R, and performs
    // the following in order to return a MatchResult
    SplitMatch = function(R, S, q)
    {
        // 1. If R is a RegExp object, go to step 8
        if (Esiply.get_class(R) == "RegExp")
        {
            // 8. Call the [[match]] method of R giving it the args
            // S and q, and return the MatchResult result
            debug("split: SplitMatch: R is regex, returning R.match\n");
            return R.match(S, q);
        }

        // 2. R must be a string. Let r be the number of chars in R
        var r = R.length;

        // 3. Let s be the number of chars in S
        var s = S.length;

        // 4. If q+r>s then return the MatchResult failure
        if (q+r > s)
        {
            return State.FAILURE;
        }

        // 5. If there exists an integer i between 0 and r such that the
        // character at position q+i of S is different from the character
        // at position i of R, the return failure.
        for (var i = 0; i < r; i++)
        {
            if (S.charAt(q+i) != R.charAt(i))
            {
                debug("split: SplitMatch: 5: mismatch at: " + Number(q+i) + "\n");
                return State.failure;
            }
        }

        // 6. Let cap be an empty array of captures
        // Note: Captures is index starting at 1.
        var cap = new Array(1);

        // 7. Return the State(q+r, cap)
        return new State(q+r, cap);
    };

    // 1. Let S = toString(this)
    var S = String(this);
    debug("split: S set to: " + S + "\n");

    // 2. Let a be a new array created as if by the expression
    // new Array()
    var A = new Array();

    // 3. If limit is undefined, let lim = 2e32-1; else set lim 
    // be toUint32(lim)
    // >>> 0 is my toUint32 operator.
    var lim = limit == undefined ? (-1 >>> 0) : limit >>> 0;
    debug("split: lim set to: " + lim + "\n");

    // 4. Let s be the number of characters in S
    var s = S.length;
    debug("split: s set to: " + s + "\n");

    // 5. Let p = 0
    var p = 0;

    // 6. If separator is a RegExp object, let R = separator, otherwise let
    // R = toString(separator)
    var R = Esiply.get_class(separator) == "RegExp" ? separator : String(separator);
    debug("split: R set to: " + R + "\n");

    // 7. If lim = 0, return A
    if (lim == 0)
    {
        debug("split: 0 limit. Returning.\n");
        return A;
    }

    // 8. If separator is undefined, go to step 33
    if (separator != undefined)
    {
        // 9. If s = 0, go to step 31
        if (s != 0)
        {
            // 10. Let q = p
            var q = p;
            debug("split: 10: q: " + q + "\n");

            step11:
            do 
            {
                // 11. If q = s, go to step 28
                if (q == s)
                {
                    debug("split: 28: q==s: " + q + "\n");

                    // 28. let T be a string value equal to the substring of S
                    // consisting of the characters at position p through s
                    var T = S.substring(p, s);
                    debug("split: 28: T: " + T + "\n");

                    // 29. Call the [[put]] method of A with arguments A.length
                    // and T
                    A[A.length] = T;

                    // 30. Return A.
                    return A;
                }

                // 12. Call SplitMatch(R, S, q) and let z be its MatchResult result
                var z = SplitMatch(R, S, q);

                // 13. If z is failure, go to step 26
                if (z != State.failure)
                {
                    // 14. z must be a State. Let e be z's endIndex and
                    // let cap be z's captures array
                    var e = z.endIndex;
                    var cap = z.captures;
                    debug("split: 14: e: " +  e + "\n");
                    debug("split: 14: cap: " + cap.join() + "\n");

                    // 15. If e = p, go to step 26
                    if (e == p)
                    {
                        debug("split: 15: e==p: " + e + "\n");

                        // 26. Let q = q+1
                        q++;

                        // 27. Go to step 11.
                        continue;
                    }

                    // 16. Let T be a string value equal to the substring
                    // of S consisting of the characters at positions p->q
                    var T = S.substring(p, q);
                    debug("split: 16: T: " + T + "\n");

                    // 17. Call the [[put]] method of A with args A.length and T
                    A[A.length] = T;

                    // 18. If A.length = lim, return A
                    if (A.length == lim)
                    {
                        return A;
                    }

                    // 19. Let p = e
                    p = e;

                    // 20. Let i = 0;
                    var i = 0;

                    // 21. If i is equal to the number of elements in cap, go
                    // to step 10.
                    do 
                    {
                        if (i == cap.length-1)
                        {
                            // 10. q = p
                            q = p;

                            // to 11...
                            continue step11;
                        }

                        // 22. let i = i+1.
                        i++;

                        // 23. call the [[put]] method of A with args A.lenght
                        // and cap[i]
                        debug("split: 23: appending cap[i]: " + cap[i] + "\n");
                        A[A.length] = cap[i];

                        // 24. If A.length = lim, return A
                        if (A.length == lim)
                        {
                            return A;
                        }

                        // 25. Go to step 21
                    } while (true);


                }

                // 26. Let q = q+1
                q++;

                // 27. Go to step 11.
            } while (true);
        }
        // 31. call SplitMatch(R, S, 0) and let z be its MatchResult result.
        z = SplitMatch(R, S, 0);

        // 32. If z is not failure, return A.
        if (z != State.FAILURE)
        {
            debug("split: 32: z is not failure\n");
            return A;
        }
    }

    // 33. Call the [[put]] method of A with arguments "0" and S
    A[0] = S;

    // 34. Return A
    return A;
};


// Ecma 15.5.4.15 String.prototype.substring(start, end)
// The substring method takes two arguments, start and and, and returns
// a substring of the result of converting this object to a string, starting
// from character positing start and running to, but not
// including, character position end of the string (or through the end of
// the string if end is undefined). The result is a string value, not
// a String object.
Esiply.set_attributes(String.prototype, "substring", Esiply.DONT_ENUM);
String.prototype.substring = function(start, end)
{
    // 1. Call tostring, giving it the this value
    var str = String(this);

    // 2. Compute the number of characters in that
    var len = str.length;

    // 3. Call toInteger(start)
    // TODO: we can't use Math.floor yet, I hope this is an integer
    start = Number(start);

    // 4. If end is undefined, use result 2, else use to integer end
    end = (end == undefined) ? len : end;

    // 5. Compute (min(max(result(3),0), result(2)))
    var res5 = Math.min(Math.max(start, 0), len);

    // 6. Compute (min(Max(result(4),0), result(2)))
    var res6 = Math.min(Math.max(end, 0), len);

    // 7. Compute min(res 5, res 6)
    var res7 = Math.min(res5, res6);
    var res8 = Math.max(res5, res6);

    var res = "";
    for (var i = res7; i < res8; i++)
    {
        res += str.charAt(i);
    }
    return res;

};

// Ecma 15.5.4.16 String.prototype.toLowerCase()
// TODO: make unicodable
Esiply.set_attributes(String.prototype, "toLowerCase", Esiply.DONT_ENUM);
String.prototype.toLowerCase = function()
{
    var str = String(this);
    var nstr = "";
    for (var i = 0; i < str.length; i++)
    {
        if (str.charAt(i) >= "A" && str.charAt(i) <= "Z")
        {
            nstr += String.fromCharCode(str.charCodeAt(i) + "a".charCodeAt(0) - "A".charCodeAt(0));
        }
        else
        {
            nstr += str.charAt(i);
        }
    }
    return nstr;
};

// Ecma 15.5.4.18 String.prototype.toUpperCase()
// TODO: make unicodable
Esiply.set_attributes(String.prototype, "toUpperCase", Esiply.DONT_ENUM);
String.prototype.toUpperCase = function()
{
    var str = String(this);
    var nstr = "";
    for (var i = 0; i < str.length; i++)
    {
        if (str.charAt(i) >= "a" && str.charAt(i) <= "z")
        {
            nstr += String.fromCharCode(str.charCodeAt(i) + "A".charCodeAt(0) - "a".charCodeAt(0));
        }
        else
        {
            nstr += str.charAt(i);
        }
    }
    return nstr;
};

// Ecma B.2.3 String.prototype.substr(start,length)
// Technically non-standard, though it is listed in the standard...
// The substr method takes two arguments, start and length, and returns
// a substring of the result of converting this object to a string, starting
// from the character position start and running for length characters
// (or through the end of the string if length is undefined). If start is
// negative, it is treated as sourceLength+start) where sourcelength is the
// length of the string. The result is a string value, not a String object.
Esiply.set_attributes(String.prototype, "substr", Esiply.DONT_ENUM);
String.prototype.substr = function(start, length)
{
    return this.substring(start, start + length);
};

